<template>
	<view class="u-upload" v-if="!disabled" :style="[customStyle]">
		<view
			class="u-list-item u-preview-wrap"
			v-if="showUploadList"
			v-for="(item, index) in lists"
			:key="index"
			:style="{
				width: $u.addUnit(width),
				height: $u.addUnit(height)
			}"
		>
			<view
				v-if="deletable"
				class="u-delete-icon"
				@tap.stop="handleRemoveFile(item, index)"
				:style="{
					background: delBgColor
				}"
			>
				<u-icon class="u-icon" :name="delIcon" size="20" :color="delColor"></u-icon>
			</view>
			<image @tap.stop="doPreviewImage(item.url, index)" class="u-preview-image" :src="item.url" :mode="imageMode"></image>
		</view>
		<slot name="file" :file="lists"></slot>
		<view style="display: inline-block;" @tap="selectFile" v-if="maxCount > lists.length" :style="{ cursor: uploadDisabled ? 'not-allowed' : '' }">
			<view
				class="u-list-item u-add-wrap"
				hover-class="u-add-wrap__hover"
				hover-stay-time="150"
				:style="{
					width: $u.addUnit(width),
					height: $u.addUnit(height)
				}"
			>
				<u-icon name="plus" class="u-add-btn" size="40"></u-icon>
				<view class="u-add-tips">{{ uploadText }}</view>
			</view>
		</view>
		<u-toast ref="toast" />
	</view>
</template>

<script>
import mpConfig from '../../libs/mixin/mp-vertual-node.js'
import Validator from './validator.js';
/**
 * upload 图片上传
 * @description 该组件用于上传图片场景
 * @tutorial https://www.uviewui.com/components/upload.html
 * @property {String} action 服务器上传地址
 * @property {String Number} max-count 最大选择图片的数量（默认99）
 * @property {Boolean} custom-btn 如果需要自定义选择图片的按钮，设置为true（默认false）
 * @property {Boolean} show-progress 是否显示进度条（默认true）
 * @property {Boolean} disabled 是否启用(显示/移仓)组件（默认false）
 * @property {String} image-mode 预览图片等显示模式，可选值为uni的image的mode属性值（默认aspectFill）
 * @property {String} del-icon 右上角删除图标名称，只能为uView内置图标
 * @property {String} del-bg-color 右上角关闭按钮的背景颜色
 * @property {String | Number} index 在各个回调事件中的最后一个参数返回，用于区别是哪一个组件的事件
 * @property {String} del-color 右上角关闭按钮图标的颜色
 * @property {Object} header 上传携带的头信息，对象形式
 * @property {Object} form-data 上传额外携带的参数
 * @property {String} name 上传文件的字段名，供后端获取使用（默认file）
 * @property {Array<String>} size-type original 原图，compressed 压缩图，默认二者都有（默认['original', 'compressed']）
 * @property {Array<String>} source-type 选择图片的来源，album-从相册选图，camera-使用相机，默认二者都有（默认['album', 'camera']）
 * @property {Boolean} preview-full-image	是否可以通过uni.previewImage预览已选择的图片（默认true）
 * @property {Boolean} multiple	是否开启图片多选，部分安卓机型不支持（默认true）
 * @property {Boolean} deletable 是否显示删除图片的按钮（默认true）
 * @property {String Number} max-size 选择单个文件的最大大小，单位B(byte)，默认不限制（默认Number.MAX_VALUE）
 * @property {Array<Object>} file-list 默认显示的图片列表，数组元素为对象，必须提供url属性
 * @property {Boolean} upload-text 选择图片按钮的提示文字（默认“选择图片”）
 * @property {Boolean} auto-upload 选择完图片是否自动上传，见上方说明（默认true）
 * @property {Boolean} show-tips 特殊情况下是否自动提示toast，见上方说明（默认true）
 * @property {Boolean} show-upload-list 是否显示组件内部的图片预览（默认true）
 * @event {Function} on-oversize 图片大小超出最大允许大小
 * @event {Function} on-preview 全屏预览图片时触发
 * @event {Function} on-remove 移除图片时触发
 * @event {Function} on-success 图片上传成功时触发
 * @event {Function} on-change 图片上传后，无论成功或者失败都会触发
 * @event {Function} on-error 图片上传失败时触发
 * @event {Function} on-progress 图片上传过程中的进度变化过程触发
 * @event {Function} on-uploaded 所有图片上传完毕触发
 * @event {Function} on-choose-complete 每次选择图片后触发，只是让外部可以得知每次选择后，内部的文件列表
 * @event {Function} on-list-change 当内部文件列表被加入文件、移除文件，或手动调用clear方法时触发
 * @event {Function} on-choose-fail 选择文件出错时触发，比如选择文件时取消了操作，只在微信和APP有效
 * @example <u-upload :action="action" :file-list="fileList" ></u-upload>
 */
export default {
	name: "u-upload",
  options: mpConfig,
	emits: ['removeSuccess', 'uploadSuccess', 'uploadFailed', 'update:fileList',  "on-choose-complete", "on-choose-fail", 'on-preview'],
	props: {
		//是否显示组件自带的图片预览功能
		showUploadList: {
			type: Boolean,
			default: true
		},
		// 后端地址
		action: {
			type: String,
			default: ""
		},
		// 最大上传数量
		maxCount: {
			type: [String, Number],
			default: 52
		},
		// 是否启用
		disabled: {
			type: Boolean,
			default: false
		},
		// 预览上传的图片时的裁剪模式，和image组件mode属性一致
		imageMode: {
			type: String,
			default: "aspectFill"
		},
		// 头部信息
		header: {
			type: Object,
			default() {
				return {};
			}
		},
		// 额外携带的参数
		formData: {
			type: Object,
			default() {
				return {};
			}
		},
		// 上传的文件字段名
		name: {
			type: String,
			default: "file"
		},
		// 所选的图片的尺寸, 可选值为original compressed
		sizeType: {
			type: Array,
			default() {
				return ["original", "compressed"];
			}
		},
		sourceType: {
			type: Array,
			default() {
				return ["album", "camera"];
			}
		},
		// 是否在点击预览图后展示全屏图片预览
		previewFullImage: {
			type: Boolean,
			default: true
		},
		// 是否开启图片多选，部分安卓机型不支持
		multiple: {
			type: Boolean,
			default: true
		},
		// 是否展示删除按钮
		deletable: {
			type: Boolean,
			default: true
		},
    // 文件大小限制，单位为byte
    maximumFileSize: {
      type: [String, Number],
      default: 2048,
    },
		// 显示已上传的文件列表
		fileList: {
			type: Array,
			default() {
				return [];
			}
		},
		// 上传区域的提示文字
		uploadText: {
			type: String,
			default: "选择图片"
		},
		// 内部预览图片区域和选择图片按钮的区域宽度
		width: {
			type: [String, Number],
			default: 200
		},
		// 内部预览图片区域和选择图片按钮的区域高度
		height: {
			type: [String, Number],
			default: 200
		},
		// 右上角关闭按钮的背景颜色
		delBgColor: {
			type: String,
			default: "#fa3534"
		},
		// 右上角关闭按钮的叉号图标的颜色
		delColor: {
			type: String,
			default: "#ffffff"
		},
		// 右上角删除图标名称，只能为uView内置图标
		delIcon: {
			type: String,
			default: "close"
		},
		// 右下角成功图标名称，只能为uView内置图标
		successIcon: {
			type: String,
			default: "checkbox-mark"
		},
		// 右下角成功的叉号图标的颜色
		successColor: {
			type: String,
			default: "#ffffff"
		},
		// 如果上传后的返回值为json字符串，是否自动转json
		toJson: {
			type: Boolean,
			default: true
		},
		// 上传前的钩子，每个文件上传前都会执行
		beforeUpload: {
			type: Function,
			default: null
		},
		// 移除文件前的钩子
		beforeRemove: {
			type: Function,
			default: null
		},
		// 允许上传的图片后缀
    accept: {
      type: String,
      default: 'image/*', // 使用原生的input的accept参数
    },
		// 支持上传文件夹
    directory: {
      type: Boolean,
      default: () => false,
    },
    // 是否仅允许上传单个文件
    isSingle: {
      type: Boolean,
      default: () => false,
    },
    // 上传列表的内建样式，支持三种基本样式 text, picture 和 picture-card
    listType: {
      type: String,
      default: () => 'text',
    },
    // 是否支持自定义文件格式
    allowedUploadFileFormat: {
      type: Boolean,
      default: () => true,
    },
    // 是否显示自定义文件格式提示内容
    isShowFormatContent: {
      type: Boolean,
      default: () => true,
    },
    // 文件上传函数
    // uploadFile: {
    //   type: Function,
    //   default: () => () => {},
    // },
    // 仅允许拍照上传
    isCamera: {
      type: Boolean,
      default: () => false,
    },
    // 增加水印
    addWatermark: {
      type: Boolean,
      default: () => false,
    },
    // 自定义增加水印方法
    customAddWaterMarkFunc: {
      type: Function,
      default: () => () => {},
    },
    // 水印文字
    watermarkText: {
      type: Array,
      default: () => [],
    },
	},
	mounted() {},
	data() {
		return {
			lists: [],
			uploading: false
		};
	},
	computed: {
    // 上传按钮是否禁用
    uploadDisabled() {
      return (this.isSingle && this.fileList.length >= 1) || this.disabled || this.uploading;
    },
    capture() {
      return this.isCamera ? ['camera'] : this.sourceType;
    },
  },
	watch: {
		fileList: {
      immediate: true,
      deep: true,
      handler(n) {
        this.lists = n;
      },
    },
    // 监听lists的变化，发出事件
    lists: {
      deep: true,
      handler(n) {
        this.$emit('update:fileList', n);
      },
    },
	},
	methods: {
		// 清除列表
		clear() {
			this.lists = [];
		},
		// 提示用户消息
		showToast(message, type='default') {
			this.$refs.toast.show({
				title: message,
				type,
			});
		},
		// 选择图片
		selectFile() {
			let that = this;
			if (that.uploadDisabled) {
				this.showToast("不支持上传", 'error')
				return
			}
			const { maxCount, multiple, sizeType, capture } = that;
			let chooseFile = null;
			let lists = JSON.parse(JSON.stringify(that.lists));
			const newMaxCount = maxCount - lists.length;
			// 设置为只选择图片的时候使用 chooseImage 来实现
			chooseFile = new Promise((resolve, reject) => {
				uni.chooseImage({
					count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
					sourceType: capture,
					sizeType,
					success: resolve,
					fail: reject
				});
			});
			chooseFile
				.then(res => {
					// 每次图片选择完，抛出一个事件，并将当前内部选择的图片数组抛出去
					that.$emit("on-choose-complete", res.tempFiles);
					this.handleUploadFiles(res.tempFiles);
				})
				.catch(error => {
					that.$emit("on-choose-fail", error);
				});
		},
		handleBeforeUpload(file) {
      const { isSingle, allowedUploadFileFormat, accept, fileList, maximumFileSize } = this;
      const verifyData = {
        file,
        fileList,
        accept,
        isSingle,
        allowedUploadFileFormat,
      };
      // 创建一个validator 对象
      const validator = new Validator(verifyData);
      // 添加校验规则
      validator.add('isSingle', '你最多可以上传1个文件!');
      validator.add('maxNumber', '最多上传十个文件!', 10);
      validator.add('isLegalType', '不支持的文件类型');
      if (maximumFileSize) {
        validator.add('maximumFileSize', `文件最大为${maximumFileSize}MB!`, maximumFileSize);
      }
      // 获得校验结果
      const errorMsg = validator.start();
      if (errorMsg) {
        this.$emit('uploadFailed', errorMsg);
        return false; // 阻止上传
      }
      // 添加水印
      if (this.addWatermark) {
        if (this.customAddWaterMarkFunc) {
          return this.customAddWaterMarkFunc(file);
        } else {
          return new Promise((resolve) => {
            const reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = () => {
              const img = document.createElement('img');
              img.src = reader.result;
              img.onload = () => {
                const canvas = document.createElement('canvas');
                canvas.width = img.naturalWidth;
                canvas.height = img.naturalHeight;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
                ctx.fillStyle = '#fff';
                ctx.textBaseline = 'middle';
                const spaceH = 4;
                this.watermarkText.forEach((item, index) => {
                  if (index === 0) {
                    // 文字样式
                    ctx.font = '400 16px PingFangSC-Regular ';
                    ctx.fillText(item, 24, 40);
                  } else {
                    ctx.font = '400 12px PingFangSC-Regular ';
                    ctx.fillText(item, 24, 40 + spaceH + index * 20);
                  }
                });
                canvas.toBlob(resolve);
              };
            };
          });
        }
      }
      return true;
    },
		async processFile(file, fileList) {
      //上传前优先判断是否满足上传要求
      // 执行before-upload钩子
      const beforeUploadFunc = this.beforeUpload ? this.beforeUpload : this.handleBeforeUpload;
      let transformedFile = file;
      if (beforeUploadFunc && typeof beforeUploadFunc === 'function') {
        try {
          transformedFile = await beforeUploadFunc(file, fileList);
        } catch (e) {
          transformedFile = false;
        }
        if (transformedFile === false) {
          return {
            origin: file,
            parsedFile: null,
          };
        }
        const parsedData =
          (typeof transformedFile === 'object' || typeof transformedFile === 'string') &&
          transformedFile
            ? transformedFile
            : file;
        let parsedFile;
        if (parsedData instanceof File) {
          parsedFile = parsedData;
        } else {
          parsedFile = new File([parsedData], file.name, { type: file.type });
        }
        const mergedParsedFile = parsedFile;
        return {
          origin: file,
          parsedFile: mergedParsedFile,
        };
      }
    },
		async handleUploadFiles(files) {
      const originFiles = [...files];
      const postFiles = originFiles.map((file) => {
        return this.processFile(file, originFiles);
      });
      Promise.all(postFiles).then((fileList) => {
        const parsedFileList = fileList.map(({ origin, parsedFile }) => ({
          file: origin,
          parsedFile,
        })).filter((file) => file.parsedFile !== null);
        this.uploadFile(parsedFileList, 0);
      });
    },
		// 上传图片
		async uploadFile(fileList, index = 0) {
			if (this.uploadDisabled) return;
			if (this.uploading) return;
			// 全部上传完成
			if (index >= fileList.length) {
				return;
			}
			this.uploading = true;
			if(!this.action) {
				this.showToast("请配置上传地址");
				return;
			}
			// 创建上传对象
			const task = uni.uploadFile({
				url: this.action,
				file: fileList[index].parsedFile,
				name: this.name,
				formData: this.formData,
				header: this.header,
				success: res => {
					// 判断是否json字符串，将其转为json格式
					let result = this.toJson && this.$u.test.jsonString(res.data) ? JSON.parse(res.data) : res.data;
					if (![200, 201, 204].includes(res.statusCode)) {
						const errorMsg = data?.message ?? data?.errorMsg ?? data;
            this.$emit('uploadFailed', errorMsg);
					} else {
						// 上传成功
						const { fileUrl = '', fullUrl = '' } = result.data ?? {};
						const fileInfo = {
                name: fileList[index].parsedFile.name,
                size: fileList[index].parsedFile.size,
                type: fileList[index].parsedFile.type,
                url: fullUrl,
                imgUrl: fileUrl,
                status: 'done',
              };
              this.lists.push(fileInfo);
							this.$emit('uploadSuccess', fileInfo);
					}
				},
				fail: e => {
					this.$emit('uploadFailed', e);
				},
				complete: res => {
					uni.hideLoading();
					this.uploading = false;
					this.uploadFile(fileList, index + 1);
				}
			});
		},
		// 删除一个图片
		handleRemoveFile(removeFile, index) {
			uni.showModal({
				title: "提示",
				content: "您确定要删除此项吗？",
				success: async res => {
					if (res.confirm) {
						// 先检查是否有定义before-remove移除前钩子
						// 执行before-remove钩子
						if (this.beforeRemove && typeof this.beforeRemove === "function") {
							// 此处钩子执行 原理同before-remove参数，见上方注释
							let beforeResponse = this.beforeRemove.bind(this.$u.$parent.call(this))(index, this.lists);
							// 判断是否返回了promise
							if (!!beforeResponse && typeof beforeResponse.then === "function") {
								await beforeResponse
									.then(res => {
										// promise返回成功，不进行动作，继续上传
										this.handlerDeleteItem(index);
									})
									.catch(err => {
										// 如果进入promise的reject，终止删除操作
										this.showToast("已终止移除");
									});
							} else if (beforeResponse === false) {
								// 返回false，终止删除
								this.showToast("已终止移除");
							} else {
								// 如果返回true，执行删除操作
								this.handlerDeleteItem(removeFile, index);
							}
						} else {
							// 如果不存在before-remove钩子，
							this.handlerDeleteItem(removeFile, index);
						}
					}
				}
			});
		},
		// 执行移除图片的动作，上方代码只是判断是否可以移除
		handlerDeleteItem(removeFile, index) {
			this.lists.splice(index, 1);
			this.$forceUpdate();
			this.$emit('removeSuccess', removeFile);
		},
		// 用户通过ref手动的形式，移除一张图片
		remove(index) {
			// 判断索引的合法范围
			if (index >= 0 && index < this.lists.length) {
				this.lists.splice(index, 1);
			}
		},
		// 预览图片
		doPreviewImage(url, index) {
			if (!this.previewFullImage) {
				return;
			}
			const images = this.lists.map(item => item.url || item.path);
			uni.previewImage({
				urls: images,
				current: url,
				success: () => {
					this.$emit("on-preview", url, this.lists, index);
				},
				fail: () => {
					this.showToast("预览图片失败");
				}
			});
		},
	}
};
</script>

<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";

.u-upload {
	@include vue-flex;
	flex-wrap: wrap;
	align-items: center;
}

.u-list-item {
	width: 200rpx;
	height: 200rpx;
	overflow: hidden;
	margin: 10rpx;
	background: rgb(244, 245, 246);
	position: relative;
	border-radius: 10rpx;
	/* #ifndef APP-NVUE */
	display: flex;
	/* #endif */
	align-items: center;
	justify-content: center;
}

.u-preview-wrap {
	border: 1px solid rgb(235, 236, 238);
}

.u-add-wrap {
	flex-direction: column;
	color: $u-content-color;
	font-size: 26rpx;
}

.u-add-tips {
	margin-top: 20rpx;
	line-height: 40rpx;
}

.u-add-wrap__hover {
	background-color: rgb(235, 236, 238);
}

.u-preview-image {
	display: block;
	width: 100%;
	height: 100%;
	border-radius: 10rpx;
}

.u-delete-icon {
	position: absolute;
	top: 6rpx;
	right: 6rpx;
	z-index: 10;
	background-color: $u-type-error;
	border-radius: 100rpx;
	width: 36rpx;
	height: 36rpx;
	@include vue-flex;
	align-items: center;
	justify-content: center;
}

.u-icon {
	@include vue-flex;
	align-items: center;
	justify-content: center;
}

.u-success-icon {
	position: absolute;
	bottom: 6rpx;
	right: 6rpx;
	z-index: 10;
	background-color: #5ac725;
	border-radius: 100rpx;
	width: 36rpx;
	height: 36rpx;
	@include vue-flex;
	align-items: center;
	justify-content: center;
}

.u-progress {
	position: absolute;
	bottom: 10rpx;
	left: 8rpx;
	right: 8rpx;
	z-index: 9;
	width: auto;
}

.u-error-btn {
	color: #ffffff;
	background-color: $u-type-error;
	font-size: 20rpx;
	padding: 4px 0;
	text-align: center;
	position: absolute;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 9;
	line-height: 1;
}

</style>
